package conn;

import gui.ClientWindow;
import thread.InputThread;
import thread.OutputThread;

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Classe per la gestione delle connessioni tra l'interfaccia grafica e il Socket. Si occupa di aprire e gestire le
 * comunicazioni con i socket, tenere una coda di messaggi, di catturare gli aggiornamenti provenienti dall'interfaccia.
 * @author Claudio "Dna" Bonesana
 */
public class SocketManager {

    private ConnectionData connectionData;
    private Socket socket;

    private List<ClientWindow> clientWindows = new ArrayList<ClientWindow>();
    private LinkedBlockingQueue<String> messages = new LinkedBlockingQueue<String>();

    private InputThread inputThread;
    private OutputThread outputThread;

    public SocketManager() {}

    /**
     * Crea un {@link SocketManager} basato sui dati di un {@link ConnectionData} valido. Prima di poter effettuare una
     * connessione con il metodo {@link #connect()} occorre impostare un'interfaccia con il metodo {@link #addClientWindow(gui.ClientWindow)}.
     * @param connectionData    un {@link ConnectionData} valido
     */
    public void setConnectionData(ConnectionData connectionData) {
        this.connectionData = connectionData;
    }

    /**
     * @return  la coda di messaggi attuale
     */
    public LinkedBlockingQueue<String> getMessages() {
        return messages;
    }

    /**
     * @param clientWindow  un'interfaccia grafica {@link ClientWindow} valida
     */
    public void addClientWindow(ClientWindow clientWindow) {
        this.clientWindows.add(clientWindow);
    }

    /**
     * Crea un nuovo {@link Socket} basato sui dati del {@link ConnectionData} fornito al costruttore, genera le thread
     * {@link InputThread} e {@link OutputThread} e le lancia.
     */
    public void connect() {
        try {
            socket = new Socket(connectionData.getServer(),connectionData.getPort());

            launchThreads();
            sendMessage(connectionData.getUsername() + '\n');

        } catch (IOException e) {
            updateClientWindow("Error connecting to the server.");
        }
    }

    /**
     * Esegue le thread e nel contempo abilità la scrittura dell'interfaccia grafica.
     * @throws IOException
     */
    public void launchThreads() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

        inputThread = new InputThread();
        inputThread.setSocketManager(this);
        inputThread.setBufferedReader(bufferedReader);

        outputThread = new OutputThread();
        outputThread.setSocketManager(this);
        outputThread.setBufferedWriter(bufferedWriter);

        inputThread.start();
        outputThread.start();

        updateClientWindow("<span style=\"color: green\">Server connected</span>");
        enableClientWindows();
    }

    /**
     * Aggiunge un messaggio alla coda di messaggi.
     * @param str   il nuovo messaggio
     */
    public void sendMessage(String str){
        messages.add(str);
    }

    /**
     * Abilita tutte le interfacce grafiche gestite a scrivere.
     */
    public void enableClientWindows(){
        for(ClientWindow clientWindow : clientWindows){
            clientWindow.enableWindow();
        }
    }

    /**
     * Disabilita tutte le interfacce grafiche gestite.
     */
    public void disableClientWindows(){
        for(ClientWindow clientWindow : clientWindows){
            clientWindow.disableWindow();
        }
    }

    /**
     * Aggiorna tutte le interfacce grafiche con il nuovo messaggio proveniente dal server. Nel caso in cui si tratti di
     * un comando di disconnessione, termina la connessione con il server.
     * @param str   il messaggio proveniente dal server
     */
    public void updateClientWindow(String str){
        if (str.equals("/kick")){
            disconnect();
        }
        for(ClientWindow clientWindow : clientWindows){
            clientWindow.update(str);
        }
    }

    /**
     * Metodo per tentare una riconnessione. Nel caso in cui il server non risponda più, disabilita le interfacce grafiche
     * per la scrittura, interrompe i thread e tenta di aprire un nuovo {@link Socket} ogni 500ms. In caso di successo rilancia
     * i thgread con i nuovi parametri e informa il server dell'avvenuta riconnessione.
     */
    public void attemptReconnect() {
        disableClientWindows();

        inputThread.stopThread();
        outputThread.stopThread();

        while (true){
            try {
                Thread.sleep(500);
                socket = new Socket(connectionData.getServer(),connectionData.getPort());
                break;
            } catch (IOException ignored) {
            } catch (InterruptedException ignored) {
            }
        }

        try {
            launchThreads();
            sendMessage("/"+connectionData.getUsername() + '\n');
        } catch (IOException ignored) {
        }
    }

    /**
     * Disconnette in sicurezza il client dal server. Aggiorna la gui e spegne i thread di comunicazione.
     */
    public void disconnect(){
        updateClientWindow("<span style=\"color: red\">Disconnected from server.</span>");
        disableClientWindows();
        inputThread.stopThread();
        outputThread.stopThread();
        try {
            socket.close();
        } catch (IOException ignored) {
        }
    }
}
